package cn.adelyn.base.auth.service;

import cn.adelyn.base.api.auth.pojo.bo.UserInfoInTokenBO;
import cn.adelyn.base.auth.constant.AuthCacheNames;
import cn.adelyn.base.auth.pojo.bo.TokenInfoBO;
import cn.adelyn.base.auth.pojo.vo.TokenInfoVO;
import cn.adelyn.common.cache.constant.CacheNames;
import cn.adelyn.common.core.cglib.CglibUtil;
import cn.adelyn.common.core.execption.AdelynException;
import cn.adelyn.common.core.response.ResponseEnum;
import cn.adelyn.common.core.util.PrincipalUtil;
import cn.adelyn.common.crypto.utils.AesUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * @author chengze
 * @date 2022/11/18
 * @desc token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
 */
@Service
@Slf4j
public class TokenService {

	/**
	 * 默认5分钟有效
	 */
	@Value("${adelyn.public.auth.token.access.expiresIn:300000}")
	private long tokenExpiresIn;

	/**
	 * 单用户最大在线 tokenPair 数量
	 */
	@Value("${adelyn.public.auth.token.maxOnline:10}")
	private int maxOnlineNum;

	private final RedisTemplate<Object, Object> redisTemplate;

	private final RedisSerializer<Object> redisSerializer;

	public TokenService(RedisTemplate<Object, Object> redisTemplate, RedisSerializer<Object> redisSerializer) {
		this.redisTemplate = redisTemplate;
		this.redisSerializer = redisSerializer;
	}

	/**
	 * 只要不超时，无限续杯
	 */
	public TokenInfoVO genAndStoreTokenPair(UserInfoInTokenBO userInfoInTokenBO) {

		String refreshToken = IdUtil.simpleUUID();
		String accessToken = IdUtil.simpleUUID();

		TokenInfoBO tokenInfoBO = new TokenInfoBO();
		tokenInfoBO.setRefreshToken(refreshToken);
		tokenInfoBO.setAccessToken(accessToken);
		tokenInfoBO.setUserInfoInTokenBO(userInfoInTokenBO);

		storeTokenPair(tokenInfoBO);

		// 返回给前端时对 token 封装并加密
		tokenInfoBO.setRefreshToken(encryptToken(refreshToken));
		tokenInfoBO.setAccessToken(encryptToken(accessToken));

		return CglibUtil.copy(tokenInfoBO, TokenInfoVO.class);
	}

	/**
	 * 刷新accessToken，返回新accessToken
	 */
	public TokenInfoVO freshTokenPair(String encryptedRefreshToken) {

		UserInfoInTokenBO userInfoInTokenBO = getUserInfoByToken(encryptedRefreshToken, true);

		return genAndStoreTokenPair(userInfoInTokenBO);
	}

	/**
	 * 删除用户全部的token
	 */
	public void deleteAllToken(Integer userType, Long userId) {

		String userIdKeyStr = getUserIdKeyStr(userType, userId);
		byte[] userIdKey = userIdKeyStr.getBytes(StandardCharsets.UTF_8);

		Map<Object, Object> tokenPairMap = redisTemplate.opsForHash().entries(userIdKeyStr);
		HashMap<byte[], byte[]> tokenPairKeyMap = new HashMap<>(16);
		HashSet<byte[]> refreshTokenSet = new HashSet<>(16);

		tokenPairMap.entrySet().iterator().forEachRemaining(entry -> {
			tokenPairKeyMap.put(
					getRefreshKey((String) entry.getKey()),
					getAccessKey((String) entry.getValue())
			);
			refreshTokenSet.add(((String) entry.getKey()).getBytes(StandardCharsets.UTF_8));
		});

		redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
			tokenPairKeyMap.entrySet().iterator().forEachRemaining(entry -> {
				connection.del(entry.getKey(), entry.getValue());
			});

			refreshTokenSet.iterator().forEachRemaining(entry -> {
				connection.hDel(userIdKey, entry);
			});
			return null;
		});
	}

	/**
	 * 删除access refresh token对,用于单设备退出登录
	 */
	public void delTokenPair(Integer userType, Long userId, String encryptedRefreshToken) {
		// 通过 userIdKey 对应的 map 获取 token 对，防止异常删除
		String userIdKeyStr = getUserIdKeyStr(userType, userId);
		String refreshTokenStr = decryptToken(encryptedRefreshToken);
		String accessTokenStr = (String) redisTemplate.opsForHash().get(userIdKeyStr, refreshTokenStr);

		// adelyn_auth:token:access: accessToken
		byte[] refreshKey = getRefreshKey(refreshTokenStr);
		// adelyn_auth:token:refresh: refreshToken
		byte[] accessKey = getAccessKey(accessTokenStr);

		byte[] userIdKey = userIdKeyStr.getBytes(StandardCharsets.UTF_8);
		byte[] refreshToken = refreshTokenStr.getBytes(StandardCharsets.UTF_8);

		redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
			connection.del(refreshKey, accessKey);
			connection.hDel(userIdKey, refreshToken);
			return null;
		});
	}

	/**
	 * 获取token中的用户信息
	 */
	public UserInfoInTokenBO getUserInfoByToken(String encryptedToken, boolean isRefreshToken) {

		String decryptTokenStr = decryptToken(encryptedToken);

		UserInfoInTokenBO userInfoInTokenBO;

		if (isRefreshToken){
			userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue().get(getRefreshKeyStr(decryptTokenStr));
		}else {
			userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue().get(getAccessKeyStr(decryptTokenStr));
		}

		// 虽然 decrypt 时判断了有效期，但缓存中的信息可能被 退出登录，禁用用户等 主动删除
		if (userInfoInTokenBO == null) {
			throw new AdelynException(ResponseEnum.TOKEN_TIMEOUT);
		}
		return userInfoInTokenBO;
	}

	private void storeTokenPair(TokenInfoBO tokenInfoBO) {

		// adelyn_auth:token:userType: snowflakeid
		String userIdKeyStr = getUserIdKeyStr(tokenInfoBO.getUserInfoInTokenBO().getUserType(), tokenInfoBO.getUserInfoInTokenBO().getUserId());
		byte[] userIdKey = userIdKeyStr.getBytes(StandardCharsets.UTF_8);
		// adelyn_auth:token:access: accessToken
		byte[] refreshKey = getRefreshKey(tokenInfoBO.getRefreshToken());
		// adelyn_auth:token:refresh: refreshToken
		byte[] accessKey = getAccessKey(tokenInfoBO.getAccessToken());

		byte[] refreshToken = tokenInfoBO.getRefreshToken().getBytes(StandardCharsets.UTF_8);
		byte[] accessToken = redisSerializer.serialize(tokenInfoBO.getAccessToken());
		byte[] userInfoInToken = redisSerializer.serialize(tokenInfoBO.getUserInfoInTokenBO());

		// 防止打爆 redis
		Long refreshTokenNum = redisTemplate.opsForHash().size(userIdKeyStr);
		if (refreshTokenNum >= maxOnlineNum) {
			deleteAllToken(tokenInfoBO.getUserInfoInTokenBO().getUserType(), tokenInfoBO.getUserInfoInTokenBO().getUserId());
		}

		redisTemplate.executePipelined((RedisCallback<Object>) connection -> {

			// 当需要禁用用户的时候，可以根据 userIdKey 查出所有 refreshToken accessToken 对
			connection.hSet(userIdKey, refreshToken, accessToken);

			// 通过 refresh_token 保存 userInfoInToken 至 redis
			connection.pSetEx(refreshKey, tokenExpiresIn, userInfoInToken);

			// 通过 access_token 保存 userInfoInToken 至 redis
			connection.pSetEx(accessKey, tokenExpiresIn, userInfoInToken);

			return null;
		});

	}

	private byte[] getAccessKey(String accessToken) {
		return getAccessKeyStr(accessToken).getBytes(StandardCharsets.UTF_8);
	}

	private byte[] getRefreshKey(String refreshToken) {
		return getRefreshKeyStr(refreshToken).getBytes(StandardCharsets.UTF_8);
	}

	private byte[] getUserIdKey(Integer userType, Long userId) {
		return getUserIdKeyStr(userType, userId).getBytes(StandardCharsets.UTF_8);
	}

	private String getAccessKeyStr(String accessToken) {
		return AuthCacheNames.ACCESS + accessToken;
	}

	private String getRefreshKeyStr(String refreshToken) {
		return AuthCacheNames.REFRESH + refreshToken;
	}

	private String getUserIdKeyStr(Integer userType , Long userId) {
		return AuthCacheNames.USERID + userType + CacheNames.UNION_KEY + userId;
	}

	private String encryptToken(String data) {
		return AesUtil.aesEncryptStr(data + System.currentTimeMillis());
	}

	private String decryptToken(String data) {
		String decryptStr;
		// 可能是refreshToken,也可能是accessToken
		String targetToken;
		try {
			decryptStr = AesUtil.aesDecryptStr(data);
			// 0-32 位为 accessToken
			targetToken = decryptStr.substring(0,32);
			// 创建token的时间，防止暴力破解
			long createTokenTime = Long.parseLong(decryptStr.substring(32,45));

			if (System.currentTimeMillis() - createTokenTime > tokenExpiresIn) {
				throw new AdelynException(ResponseEnum.TOKEN_TIMEOUT);
			}
		} catch (NumberFormatException e) {
			throw new AdelynException(ResponseEnum.TOKEN_DECRYPT_FAIL, e);
		}

		// 防止解密后的token是脚本，从而对redis进行攻击
		if (!PrincipalUtil.isSimpleChar(targetToken)) {
			throw new AdelynException(ResponseEnum.TOKEN_DECRYPT_FAIL);
		}
		return targetToken;
	}

}